/************************************************************************
  DISCLAIMER:
  This software was produced by the National Institute of Standards
  and Technology (NIST), an agency of the U.S. government, and by statute
  is not subject to copyright in the United States.  Recipients of this
  software assume all responsibility associated with its operation,
  modification, maintenance, and subsequent redistribution.

  See NIST Administration Manual 4.09.07 b and Appendix I.
************************************************************************/

/*

This file contains all the source code for building a utility that
reads a marked parser test files that conform to the syntax requirements
of full DMIS and produces new parser test files for any allowed
combination of DMIS conformance modules.

The executable built from this source code takes as arguments:
1. either:
   a. the name of a DMIS input file to read (name must end in .dmi) or
   b. the name of a file containing a list of names of DMIS input files
2. the name of a directory in which to write outgoing files
3. the names of one to eight DMIS conformance modules

The file names may include absolute or relative paths.

Each DMIS code line of each DMIS input file read by this utility is
expected to be preceded by a comment line naming the minimum conformance
modules needed to handle the DMIS code line. In addition, the first line
of each file is expected to name the minimum conformance modules needed
to make the file worth using.

For example the following nine lines are the file badtst1.dmi.

$$ PM3 TW2
$$ PM1 TW1
DMISMN / 'DMIS parser test for BADTST', 05.2, PM,3, TW,2
$$ PM3 TW2
BADTST/ON
$$ PM3 TW2
BADTST/OFF
$$ PM1 TW1
ENDFIL

The first line is "$$ PM3 TW2". That means that in order to be worth
using this file for testing the BADTST statement, either level 3 of the
prismatic AP or level 2 of the thin-walled AP must be used as an
argument when calling the executable.

The second line is "$$ PM1 TW1" because either level 1 of the
prismatic AP or level 1 of the thin-walled AP is adequate for handling
the DMISMN statement on the third line.

In the markings, any PM and TW levels are alternatives, but if other
conformance module names are given, they are required. For example,
the line "$$ PM2 TW2 RY1" would mean that level 1 of the rotary table
addendum is required and either level 2 of the prismatic AP or level 2
of the thin-walled AP is also required.

All 254 DMIS input files in the parserTestFiles/okIn directory are
marked appropriately to be used as incoming files. No other marked files
are available.

The directory in which the outgoing file or files should appear must
already exist.

The original author of this file was Steven Hwang.

*/

#include "string.h"
#include "stdio.h"
#include "stdlib.h"
#include "ctype.h"


/* function declarations *******************************************/

bool checkForReq(const char * req, char level);
void checkForSpaces(char * buffer);
void compareReq(char * fileRequirements, FILE * inFile);
int conformanceChecker(char * fileLine);
void makeNewName
  (char * newName, char * outgoingDirectory, char * incomingFileName);
void printInConformed
  (char * incomingFileName, char * outgoingDirectory, char * fileRequirements);
void printUsageErr(char * executable);
void processDmisFile(char * incomingFileName, char * outgoingDirectory);

/* global Variables *************************************************/

int numMod = 0;               // number of conformance modules
char * conformanceModules[9]; // strings giving names of conformance modules

/* function definitions *********************************************/

/* checkForReq

Returned Value: boolean

This checks if the level of one requirement from an incoming file line
is less than or equal to the level of a requirement of the same base
name given in the conformanceModules.  If so, this returns true.
Otherwise it returns false.

The module names in the conformanceModules array always have 3 chars.
The first two are the base name and the third is a digit giving the level.
The digit is always 1, 2, or 3.
An example of a name that might be in the conformanceModules is "PM2".

The req always consists of two chars, which are expected to be the base
name of a conformance module.

Called By:  conformanceChecker

*/

bool checkForReq(  /* ARGUMENTS                                          */
 const char * req, /* base name of a module requirement from a file line */
 char level)       /* level of a module requirement from a file line     */
{
  int i;

  if (level == '4')
    { // no conformanceModule is at level 4
      return false;
    }
  for (i = 0; i < numMod; i++)
    {
      if ((req[0] == conformanceModules[i][0]) &&
	  (req[1] == conformanceModules[i][1]))
	{ // name matches, so check level
	  if (level <= conformanceModules[i][2])
	    return true;
	  else
	    return false;
	}
    }
  return false; // name never matched
}

/**************************************************************************/

/* checkForSpaces

Returned Value: none

Called By:  compareReq

This function removes any spaces that may be at the end of the
buffer. Spaces at the end cause errors elsewhere in the program.

*/

void checkForSpaces( /* ARGUMENTS             */
 char * buffer)      /* the line requirements */
{
  int length;

  for (length = strlen(buffer); (int)buffer[length-3] == 32; length--)
    {
      buffer[length-3] = '\r';
      buffer[length-2] = '\n';
      buffer[length-1] = 0;
    }
}

/**************************************************************************/

/* compareReq

Returned Value: none

Called By:  processDmisFile

This builds the fileRequirements array by reading and processing
the lines of the file. A line of the file is used for updating the
data only if the line is a comment line and its requirements do
not exceed the requirements in conformanceModules (since the line
will not be put into the outgoing file if it exceeds them).

The maximum values (over all the lines that are used for updating) of
each of the ten possible entries on a comment line are found. In all
cases except IP, QI, and IPQI, the maximum entry value is used to print
the fileRequirements.

For IP, QI, and IPQI, if ipqi is not 0 after the file has been processed:
1. If ip is not 0, the larger of the ipqi and ip values is used for IP.
2. If both ip and qi are 0, the value of ipqi is used for IP.
3. Otherwise (ip is 0 and qi is not 0), the larger of the ipqi and
   qi values is used for QI.

The fileRequirements are built in the order PM TW RY MC IP QI MU SF.
The fileRequirements is terminated with a 0 (not /r or /n).

*/

void compareReq(          /* ARGUMENTS                                   */
 char * fileRequirements, /* module requirements of the file, built here */
 FILE * inFile)           /* file being processed                        */
{
  int k;              // number of characters read by sscanf
  int i;              // counter for char arrays
  int length;         // length of fileRequirements string
  char pm = '0';      // max PM level found on kept lines
  char tw = '0';      // max TW level found on kept lines
  char ry = '0';      // max RY level found on kept lines
  char mc = '0';      // max MC level found on kept lines
  char ct = '0';      // max CT level found on kept lines
  char ip = '0';      // max IP level found on kept lines
  char qi = '0';      // max QI level found on kept lines
  char ipqi = '0';    // max IPQI level found on kept lines
  char mu = '0';      // max MU level found on kept lines
  char sf = '0';      // max SF level found on kept lines
  char lineReq[15];       // one file requirement or line requirement
  static char lineRequirements[2000]; // buffer to read a file line into

  while ((fgets(lineRequirements, 2000, inFile)))
    { // read lines and set max levels found on lines that will be kept
      if (lineRequirements[0] != '$')
	{ // not a comment line, so do not process
	  continue;
	}
      checkForSpaces(lineRequirements);
      if (conformanceChecker(lineRequirements) != 0)
	{ // module requirements not met, so do not process
	  continue;
	}
      for (i = 3; lineRequirements[i] != 13; i = i + k) // skip $$ at beginning
	{ // read another requirement each time around the loop
	  sscanf(lineRequirements+i, "%s%n ", lineReq, &k);
	  if (strncmp(lineReq, "IPQI", 4) == 0)
	    {
	      if (lineReq[4] > ipqi)
		ipqi = lineReq[4];
	    }
	  else if (strncmp(lineReq, "IP", 2) == 0)
	    {
	      if (lineReq[2] > ip)
		ip = lineReq[2];
	    }
	  else if (strncmp(lineReq, "QI", 2) == 0)
	    {
	      if (lineReq[2] > qi)
		qi = lineReq[2];
	    }
	  else if (strncmp(lineReq, "PM", 2) == 0)
	    {
	      if (lineReq[2] > pm)
		pm = lineReq[2];
	    }
	  else if (strncmp(lineReq, "TW", 2) == 0)
	    {
	      if (lineReq[2] > tw)
		tw = lineReq[2];
	    }
	  else if (strncmp(lineReq, "RY", 2) == 0)
	    {
	      if (lineReq[2] > ry)
		ry = lineReq[2];
	    }
	  else if (strncmp(lineReq, "MC", 2) == 0)
	    {
	      if (lineReq[2] > mc)
		mc = lineReq[2];
	    }
	  else if (strncmp(lineReq, "CT", 2) == 0)
	    {
	      if (lineReq[2] > ct)
		ct = lineReq[2];
	    }
	  else if (strncmp(lineReq, "MU", 2) == 0)
	    {
	      if (lineReq[2] > mu)
		mu = lineReq[2];
	    }
	  else if (strncmp(lineReq, "SF", 2) == 0)
	    {
	      if (lineReq[2] > sf)
		sf = lineReq[2];
	    }
	}
    }
  sprintf(fileRequirements, "$$ PM%c TW%c", pm, tw);
  length = 10;
  if (ry != '0')
    {
      sprintf((fileRequirements + length), " RY%c", ry);
      length += 4;
    }
  if (mc != '0')
    {
      sprintf((fileRequirements + length), " MC%c", mc);
      length += 4;
    }
  if (ct != '0')
    {
      sprintf((fileRequirements + length), " CT%c", ct);
      length += 4;
    }
  if (ip != '0')
    {
      if (ipqi > ip)
	ip = ipqi;
      sprintf((fileRequirements + length), " IP%c", ip);
      length += 4;
    }
  else if (qi == '0')
    { // ip and qi are both 0
      if (ipqi != '0')
	{
	  sprintf((fileRequirements + length), " IP%c", ipqi);
	  length += 4;
	}
    }
  if (qi != '0')
    {
      if ((ip == '0') && (ipqi > qi))
	qi = ipqi;
      sprintf((fileRequirements + length), " QI%c", qi);
      length += 4;
    }
  if (mu != '0')
    {
      sprintf((fileRequirements + length), " MU%c", mu);
      length += 4;
    }
  if (sf != '0')
    {
      sprintf((fileRequirements + length), " SF%c", sf);
    }
}

/**************************************************************************/

/* conformanceChecker

Returned Value: int

If a line module requirements conform to the given conformance class, 0 is
returned. Otherwise, 1 is returned.

Called By:
  processDmisFile
  printInConformed

This checks one line of an incoming file for conformance to the
conformanceModules.  The line is parsed between spaces and each
separate string that results is checked (as a file line's module
requirement) against the conformanceModules.

Three cases are covered.

1. PMx and TWx on a file line are alternatives. If either one satisfies
the requirements of the conformanceModules, that is sufficient.
This keeps track of whether PM and TW have been tested, and, if they
have been tested, whether they passed or failed. This returns 1 if either
both were tested and both failed or only one was tested and it failed.

2. File lines may have the requirement IPQIx, but the conformanceModules
can have only either IPx or QIx. If IPQIx is found on a file line, first
IPx is tested. If the first test fails, QIx is tested. If that also fails,
this returns 1.

3. If a file line has a requirement that is not PMx, TWx, or IPQIx and
it fails, this returns 1.

This returns 0 if it does not return 1 for any of the reasons just given.

*/

int conformanceChecker( /* ARGUMENTS   */
 char * fileLine)       /* a file line */
{
  int g;           // number of characters read for one conformance requirement
  int j;           // line index
  char ml[20];     // a conformance requirement from the fileLine
  int pmCheck = 0; // indicator for PM, 0=not tested, 1=passed, -1=failed
  int twCheck = 0; // indicator for TW, 0=not tested, 1=passed, -1=failed

  for (j = 0; fileLine[j] != '\r'; j = (j + g))
    {
      sscanf(fileLine+j, "%s%n ", ml, &g);
      if (strcmp(ml, "$$") != 0)
        {
          if (ml[0] == 'P' && ml[1] == 'M')
            {
	      pmCheck = (checkForReq("PM", ml[2]) ? 1 : -1);
	    }
          else if (ml[0] == 'T' && ml[1] == 'W')
            {
	      twCheck = (checkForReq("TW", ml[2]) ? 1 : -1);
	    }
          else if (ml[0] == 'I' && ml[1] == 'P' && ml[2] == 'Q' && ml[3] == 'I')
            {
              if (checkForReq("IP", ml[4]));
	      else if (checkForReq("QI", ml[4]));
	      else
		return 1;
            }
          else if (!(checkForReq(ml, ml[2])))
            {
              return 1;
            }
        }
    }
  return ((((pmCheck ==  0) && (twCheck == -1)) ||
	   ((pmCheck == -1) && (twCheck ==  0)) ||
	   ((pmCheck == -1) && (twCheck == -1))) ? 1 : 0);
}

/**************************************************************************/

/* makeNewName

Returned Value: None

Called By:  printInConformed

This makes the newName.

The incomingFileName may or may not include a path. This finds the part
of the incomingFileName that follows that last slash or backslash. Call
that part endName.

This constructs the newName from three pieces in order:
1. the outgoingDirectory
2. a forward slash (ASCII forty-seven) or backslash (ASCII 92)
3. the endName

That will work for Linux, Sun, and Windows, due to use of #ifdef _WIN32.
ifdef and plat_slash added by MAP 2/13/11

*/

void makeNewName(
 char * newName,
 char * outgoingDirectory,
 char * incomingFileName)
{
  int len;
  int n;

  len = strlen(incomingFileName);
  for (n = (len - 1); n > 0; n--)
    {
      if ((incomingFileName[n] == '/') || (incomingFileName[n] == 92))
	{
	  n++;
	  break;
	}
    }
  #ifdef _WIN32
  const char plat_slash = 92;  //backslash
  #else
  const char plat_slash = 47;  //forward slash
  #endif
  sprintf(newName, "%s%c%s", outgoingDirectory, plat_slash, incomingFileName + n);
}

/**************************************************************************/

/* printInConformed

Returned Value: None

Called By:  processDmisFile

This prints the outgoing file.

A requirement line has two '$$' before a set of conformance module
names.  A requirement line describes the module requirements of the
line that follows it. If a requirement line meets the conformanceModules
requirements, the requirement line and the line that follows it
are both printed into the outgoing file. If the line that follows it
is printed and is continued (i.e., ends with $), additional lines
immediately following which start with a space are also printed.
No check is being made for the $ continuation sign; it is assumed
that lines starting with a space are continuation lines.

The outgoing file has the same name as the incoming file, but it is
placed in the outgoingDirectory (which should not be the same as the
directory in which the incoming file is found).

The fileRequirements argument is a string containing the names of the
minimum requirements found by examining the lines of the file that
meet the conformanceModules requirements. The fileRequirements may
be less than the conformanceModules requirements. The module
requirements on the DMISMN line of the file are replaced by the
those in the fileRequirements. The fileRequirements are found
in processDmisFile before this function is called.

*/

void printInConformed(     /* ARGUMENTS                     */
 char * incomingFileName,  /* name of incoming test file    */
 char * outgoingDirectory, /* path to outgoing directory    */
 char * fileRequirements)  /* minimum AP and Addenda needed */
{
  FILE * inFile;
  FILE * newFile;
  int conforms;
  int j = 0;
  int g = 0;
  char * result;
  char * pmStart;
  char buf[2000];
  char firstHalf[80] = {};
  char formatedReq[8] = {};
  char newName[300];        // path to outgoing file
  char secondHalf[32] = {};
  char subLineReq[15] = {};

  inFile = fopen(incomingFileName, "rb");
  if (inFile == 0)
    {
      fprintf(stderr, "Unable to open file %s for reading.\n"
              "Make sure the file exists\n", incomingFileName);
      exit(1);
    }
  makeNewName(newName, outgoingDirectory, incomingFileName);
  newFile = fopen(newName, "w+b");
  if (newFile == 0)
    {
      fprintf(stderr, "Unable to open file %s for writing.\n"
              "Make sure the directory exists\n", newName);
      exit(1);
    }
  result = fgets(buf, 2000, inFile);
  fprintf(newFile, "%s", buf);

  while ((result = fgets(buf, 2000, inFile)))
    {
      if (buf[0] == '$')
        {
          conforms = conformanceChecker(buf);
          if (conforms == 0)
            {
              fprintf(newFile, "%s", buf);
              result = fgets(buf, 2000, inFile);
              if (result == 0)
                {
		  fprintf(stderr, "line missing after comment line\n");
		  exit(1);
                }
              if (strncmp(buf, "DMISMN", 6) == 0)
                {
                  pmStart = strstr(buf, "PM");
		  if (pmStart == 0)
		    {
		      fprintf(stderr, "PM missing on DMISMN line\n");
		      exit(1);
		    }
		  *pmStart = 0;
                  sprintf(firstHalf, buf);
		  *pmStart = 'P';
                  for (j = 3; (fileRequirements[j] != 0); )
		    {
		      sscanf(fileRequirements+j, "%s%n ", subLineReq, &g);
		      j = j + g;
		      sprintf(formatedReq, "%c%c,%c%s", subLineReq[0],
			      subLineReq[1], subLineReq[2],
			      ((fileRequirements[j] == 0) ? "" : ", "));
		      strcat(secondHalf, formatedReq);
		    }
                  fprintf(newFile, "%s%s\r\n", firstHalf, secondHalf);
                }
              else
                {
                  fprintf(newFile, "%s", buf);
                }
            }
        }
      if ((buf[0] == ' ') && (conforms == 0))
        { // continued lines (only) start with a space; print if conforms is 0
	  fprintf(newFile, "%s", buf);
        }
    }
  fclose(newFile);
}

/**************************************************************************/

/* printUsageErr

Returned Value: None

Called By:
  main

Prints a message explaining how to call the dmisTestFileReductor.

*/

void printUsageErr( /* ARGUMENTS          */
 char * executable) /* name of executable */
{
  fprintf(stderr,
          "usage: %s <file name> <directory> <AP> [<addendum> ...]\n",
	  executable);
  fprintf(stderr,
	  "AP must be one of PM1, PM2, PM3, TW1, TW2, TW3\n");
  fprintf(stderr,
          "addendum may be 0 or 1 from each of the following 7 sets\n");
  fprintf(stderr,
          "(RY1,RY2,RY3),(CT1,CT2,CT3),(MC1,MC2,MC3),(IP1,IP2,IP3)\n");
  fprintf(stderr, "(QI1,QI2,QI3),(MU1,MU2,MU3),(SF1,SF2,SF3)\n");
  fprintf(stderr,
          "Example: %s runAllFull.txt outgoing PM3 RY2\n", executable);
  exit(1);
}

/************************************************************************/

/* processDmisFile

Returned Value: none

Called By:  main

This examines the incoming file, decides whether there should be an
outgoing file, and prints the outgoing file if so.

The fileRequirements variable used here is a character array that will
contain information about the minimum levels of AP and addenda needed
to execute the file. The information will be written at the end of
the DMISMN line of the outgoing file.

This initially checks the incoming file's first line to see if it
meets the requirements of the conformanceModules. If a file's first line
fails to meet the requirements of the conformanceModules, no outgoing file
is written.

If an outgoing file is to be written:
1. The fileRequirements is updated by calling compareReq.
2. Then printInConformed is called to print the new file.

*/

void processDmisFile(      /* ARGUMENTS                  */
 char * incomingFileName,  /* name of incoming test file */
 char * outgoingDirectory) /* path to outgoing directory */
{
  FILE * inFile;
  int conforms;
  static char buf[2000];                 // buffer to read a file line into
  static char fileRequirements[32] = {}; // module requirements
  char * result;                         // pointer to buf or 0

  inFile = fopen(incomingFileName, "rb");
  if (inFile == NULL)
    {
      fprintf(stderr, "\nUnable to open file %s for reading.\n",
	      incomingFileName);
      exit(1);
    }
  result = fgets(buf, 2000, inFile);
  conforms = conformanceChecker(result);
  if (conforms == 0)
    {
      compareReq(fileRequirements, inFile);
      fclose(inFile);
      printInConformed(incomingFileName, outgoingDirectory, fileRequirements);
    }
  else
    {
      fprintf(stderr,
	      "\nFile not generated for %s because\n", incomingFileName);
      fprintf(stderr,
	  "file requirements did not meet conformance class requirements\n");
    }
}

/************************************************************************/

/* main

Returned Value: int

Returns 1 if there are no errors in the main; otherwise returns 0 to
denote successful execution of the main.

Called By:
  None

This is the start of the program. Main checks for the following user
generated errors and exits if any occur.

1. Duplicate APs or Addendums are used in the arguments.
2. The third argument is not PM1, PM2, PM3, TW1, TW2, or TW3.
3. The fourth and succeeding arguments are not the name of a level
   of an addendum.
4. There are fewer than 4 arguments.
5. The incoming file cannot be opened.

If the incoming file ends in .dmi, it is assumed to be a DMIS input
file, and it is processed by processDmisFile. Otherwise, the incoming
file is assumed to contain a list of the names of DMIS input files,
and each file in the list is processed by processDmisFile.

*/

int main(       /* ARGUMENTS                      */
 int argc,      /* the number of arguments plus 1 */
 char * argv[]) /* executable name and arguments  */
{
  FILE * inFile;
  char buf[255];
  char * fileName;
  char * outgoingDirectory;
  char * result;
  int i;
  int j;

  numMod = argc-3;
  if ((numMod > 8) || (numMod < 1))
    {
      printUsageErr(argv[0]);
    }
  fileName = argv[1];
  outgoingDirectory = argv[2];
  for (i = 3; i < argc; i++)
    {
      for (j = i+1; j < argc; j++)
	{
	  if ((argv[i][0] == argv[j][0]) && (argv[i][1] == argv[j][1]))
	    {
	      fprintf(stderr, "There is a duplicate instance"
		      "of an AP or Addendum\n");
	      printUsageErr(argv[0]);
	    }
	}
    }
  if (strcmp(argv[3], "PM1") == 0 || strcmp(argv[3], "TW1") == 0 ||
      strcmp(argv[3], "PM2") == 0 || strcmp(argv[3], "TW2") == 0 ||
      strcmp(argv[3], "PM3") == 0 || strcmp(argv[3], "TW3") == 0)
    {
      conformanceModules[0] = argv[3];
    }
  else
    {
      printUsageErr(argv[0]);
    }
  for (i = 4; i < (argc); i++)
    {
      if ((strcmp(argv[i], "CT1")  == 0) ||
	  (strcmp(argv[i], "CT2")  == 0) ||
	  (strcmp(argv[i], "CT3")  == 0) ||

	  (strcmp(argv[i], "IP1")  == 0) ||
	  (strcmp(argv[i], "IP2")  == 0) ||
	  (strcmp(argv[i], "IP3")  == 0) ||

	  (strcmp(argv[i], "MC1")  == 0) ||
	  (strcmp(argv[i], "MC2")  == 0) ||
	  (strcmp(argv[i], "MC3")  == 0) ||

	  (strcmp(argv[i], "QI1")  == 0) ||
	  (strcmp(argv[i], "QI2")  == 0) ||
	  (strcmp(argv[i], "QI3")  == 0) ||

	  (strcmp(argv[i], "RY1")  == 0) ||
	  (strcmp(argv[i], "RY2")  == 0) ||
	  (strcmp(argv[i], "RY3")  == 0) ||

	  (strcmp(argv[i], "SF1")  == 0) ||
	  (strcmp(argv[i], "SF2")  == 0) ||
	  (strcmp(argv[i], "SF3")  == 0) ||

	  (strcmp(argv[i], "MU1")  == 0) ||
	  (strcmp(argv[i], "MU2")  == 0) ||
	  (strcmp(argv[i], "MU3")  == 0))
	{
	  conformanceModules[i-3] = argv[i];
	}
      else
	{
	  printUsageErr(argv[0]);
	}
    }
  inFile = fopen(fileName, "rb");
  if (inFile == 0)
    {
      fprintf(stderr, "\nUnable to open file %s for reading.\n"
              "Make sure the file exists\n", argv[1]);
      exit(1);
    }
  i = strlen(fileName);
  if ((fileName[i-4] == '.') &&
      ((fileName[i-3] == 'd') || (fileName[i-3] == 'D')) &&
      ((fileName[i-2] == 'm') || (fileName[i-2] == 'M')) &&
      ((fileName[i-1] == 'i') || (fileName[i-1] == 'I')))
    {
      processDmisFile(fileName, outgoingDirectory);
    }
  else
    {
      while ((result = fgets(buf, 2000, inFile)))
        {
          if (result != 0)
            {
              j = strlen(buf);

              if (buf[j-2] == 13)
                {
                  buf[j-2] = 0;
                }
              else if (buf[j-1] == 10)
                {
                  buf[j-1] = 0;
                }
              processDmisFile(buf, outgoingDirectory);
            }
        }
    }
  return 0;
}

/************************************************************************/

